package com.coincalf.wallet.task.syncblock;

import com.coincalf.dto.CoinInfoDto;
import com.coincalf.feign.CoinServiceClient;
import com.coincalf.framework.enums.RechargeStatus;
import com.coincalf.wallet.blockchain.BlockConstant;
import com.coincalf.wallet.blockchain.client.Web3jSingleton;
import com.coincalf.wallet.entity.CoinRecharge;
import com.coincalf.wallet.entity.SyncBlockHeight;
import com.coincalf.wallet.entity.UserAddress;
import com.coincalf.wallet.service.CoinRechargeService;
import com.coincalf.wallet.service.SyncBlockHeightService;
import com.coincalf.wallet.service.UserAddressService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.web3j.protocol.core.DefaultBlockParameterNumber;
import org.web3j.protocol.core.methods.response.EthBlock;
import org.web3j.protocol.core.methods.response.Transaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.*;

import static java.math.BigDecimal.ROUND_HALF_UP;

/**
 * 同步以太坊区块
 */
@Component
@EnableScheduling
@Transactional(rollbackFor = Exception.class)
public class SyncEthNewBlock {

    private static final Logger log = LoggerFactory.getLogger(SyncEthNewBlock.class);

    @Autowired
    private UserAddressService userAddressService;
    @Autowired
    private CoinRechargeService rechargeService;
    @Autowired
    private SyncBlockHeightService syncBlockHeightService;
    @Autowired
    private CoinServiceClient coinServiceClient;


    /**
     * 同步ETH区块
     */
    @Scheduled(cron = "*/5 * * * * ?")
    public void syncEthBlock() {
        long startTime = System.currentTimeMillis();
        //从数据库获取已经同步的区块高度
        SyncBlockHeight ethSyncedBlockHeight = syncBlockHeightService.selectOneByCoinName("ETH");
        if (ethSyncedBlockHeight == null || ethSyncedBlockHeight.getSyncedBlockHeight() == null) {
            log.warn("无法找到ETH同步缓存高度");
            return;
        } else if (ethSyncedBlockHeight.getStatus() == 0) {
            log.info("本次ETH同步状态已关闭");
            return;
        }
        int height = ethSyncedBlockHeight.getSyncedBlockHeight();

        //调用RPC接口查询区块信息
        EthBlock.Block block;
        long newestBlock;
        try {
            newestBlock = Web3jSingleton.getWeb3j().ethBlockNumber().send().getBlockNumber().longValue();
            block = Web3jSingleton.getWeb3j().ethGetBlockByNumber(new DefaultBlockParameterNumber(height), true).send().getBlock();
        } catch (Exception e) {
            log.error("ETH EPC调用失败 e={}", e);
            return;
        }
        if (height >= newestBlock) {
            log.info("已同步ETH高度追上最新高度啦 已同步ETH区块高度={} 当前ETH最新区块高度={}", height, newestBlock);
            return;
        }
        //区块所有交易详情
        List<EthBlock.TransactionResult> txs = block.getTransactions();
        BigInteger timestamp = block.getTimestamp();
        //出块时间(原本精确到秒所以需要乘以1000)
        Date date = new Date(timestamp.longValue() * 1000);
        //获取所有用户ETH地址 一张表 userAddress存储所有用户地址与uid
        HashMap<String, Long> userAddress = new HashMap<>(2048);
        //从数据库获取ERC20代币合约地址
        List<CoinInfoDto> contractAddresses = coinServiceClient.selectCoinInfoByMainCoinId(ethSyncedBlockHeight.getCoinId());
        List<CoinInfoDto> contractAddressesLowerCase = new ArrayList<>(contractAddresses.size());
        for (CoinInfoDto contractAddress : contractAddresses) {
            String address = contractAddress.getCoinContractAddress().toLowerCase();
            userAddress.put(address, -1L);
            //合约地址全部转成小写
            contractAddressesLowerCase.add(contractAddress);
        }

        //用户ETH地址列表
        List<UserAddress> userAddresses = userAddressService.selectListByCoinId(ethSyncedBlockHeight.getCoinId());
        for (UserAddress address : userAddresses) {
            String chainAddress = address.getAddress();
            userAddress.put(chainAddress.toLowerCase(), address.getUserId());
        }
        int size = txs.size();
        if (size > 0) {
            for (EthBlock.TransactionResult tx1 : txs) {
                Transaction tx = (EthBlock.TransactionObject) tx1;
                //构建交易表
                CoinRecharge coinRecharge = new CoinRecharge();
                String toAddress = tx.getTo();
                if (userAddress.containsKey(toAddress)) {
                    String fromAddress = tx.getFrom();
                    BigDecimal value;
                    String hash = tx.getHash();
                    //代币合约转账交易
                    CoinInfoDto coinConfigDto = containsErc20(contractAddressesLowerCase, toAddress);
                    if (coinConfigDto != null) {//代币交易记录详情都在 input 里面 接下来对 input 进行分解
                        String input = tx.getInput();
                        if ("0x".equalsIgnoreCase(input)) {
                            //交易失败的时候，input就是"0x"
                            continue;
                        }
                        //收款地址
                        try {
                            toAddress = "0x" + input.substring(34, 74);
                        } catch (Exception e) {
                            //解析失败 说明交易失败
                            log.warn("非法交易 txId={} input={}", hash, input);
                            continue;
                        }
                        if (containsErc20(contractAddressesLowerCase, toAddress) != null) {
                            //收款地址是合约地址的时候，忽略该记录 可能存在用户异常操作...
                            log.warn("转账到ERC20合约地址交易 不做处理 txHash={}", hash);
                            continue;
                        }
                        //只查询应内用户的ERC20交易
                        if (!userAddress.containsKey(toAddress)) {
                            continue;
                        }
                        log.info("有查询到ERC20充币交易 txHash={} 充币收款地址={} 合约地址={}", hash, toAddress, coinConfigDto.getCoinContractAddress());
                        String value2 = new BigInteger(input.substring(74), 16).toString();
                        //USDT-ERC20精度不一样
                        if (coinConfigDto.getCoinContractAddress().equalsIgnoreCase(BlockConstant.USDT_ERC20_ADDRESS)) {
                            value = SyncEthNewBlock.usdtToEth(value2);
                        } else {
                            value = SyncEthNewBlock.toEth(value2);
                        }
                        coinRecharge.setCoinId(coinConfigDto.getCoinId());
                        coinRecharge.setCoinName(coinConfigDto.getCoinName());
                        coinRecharge.setContractAddress(coinConfigDto.getCoinContractAddress());
                    } else {
                        log.info("有查询到ETH交易 txHash={} 充币收款地址={}", hash, toAddress);
                        value = SyncEthNewBlock.toEth(tx.getValue().toString());
                        coinRecharge.setCoinId(ethSyncedBlockHeight.getCoinId());
                        coinRecharge.setCoinName("ETH");
                    }

                    coinRecharge.setFromAddress(fromAddress);
                    coinRecharge.setAddress(toAddress);
                    Long toUserId = userAddress.get(toAddress);
                    coinRecharge.setUserId(toUserId != null ? toUserId : -1L);
                    coinRecharge.setTxid(hash);

                    //查询交易是否已经存在
                    CoinRecharge recharge = rechargeService.selectOneByCoinIdAndTxIdAndToAddress(
                            coinRecharge.getCoinId(),
                            hash,
                            toAddress
                    );
                    if (recharge != null) {
                        log.info("mysql已存在该交易，跳过啦!~");
                        continue;
                    }
                    //查询交易是否成功
                    Optional<TransactionReceipt> transactionReceipt;
                    try {
                        transactionReceipt = Web3jSingleton.getAdmin().ethGetTransactionReceipt(tx.getHash()).send().getTransactionReceipt();
                    } catch (IOException e) {
                        log.warn("{}交易状态查询失败 txHash={}", coinRecharge.getCoinName(), hash);
                        return;
                    }
                    Boolean success = transactionReceipt.map(TransactionReceipt::getStatus).map("0x1"::equals).orElse(null);
                    if (success == null) {
                        //这里其实不可能进入 除非节点未同步好
                        log.info("同步区块任务未查到交易收据，txId={}", hash);
                        continue;
                    } else {
                        if (success) {
                            //区块确认数
                            long confirmations = newestBlock - tx.getBlockNumber().longValue() + 1;
                            coinRecharge.setStatus(RechargeStatus.PENDING.getCode());
                            coinRecharge.setConfirm(Math.toIntExact(confirmations));
                        } else {
                            coinRecharge.setStatus(-1);
                        }
                    }

                    coinRecharge.setMum(value);
                    coinRecharge.setBlockNumber(height);
                    //手续费
                    BigDecimal gasPrice = new BigDecimal(tx.getGasPrice());
                    BigInteger gasUsed = transactionReceipt.map(TransactionReceipt::getGasUsed).get();
                    BigDecimal fee = gasPrice.multiply(BigDecimal.valueOf(gasUsed.longValue())).divide(BlockConstant.WEI, 8, ROUND_HALF_UP);
                    coinRecharge.setFee(fee);
                    coinRecharge.setNum(fee.add(value));
                    coinRecharge.setCollected(0);
                    //将ethTran写入数据库
                    coinRecharge.setCreated(date);
                    coinRecharge.setLastUpdateTime(new Date());
                    rechargeService.insert(coinRecharge);
                    log.info("写入ETH系列充币交易数据到数据库啦!~~ coinRecharge={}", coinRecharge);
                }
            }
        } else {
            log.info("该区块未查询到ETH交易 height={}", height);
        }
        height = height + 1;
        ethSyncedBlockHeight.setSyncedBlockHeight(height);
        ethSyncedBlockHeight.setLastUpdateTime(new Date());
        syncBlockHeightService.updateById(ethSyncedBlockHeight);

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        long spent = (endTime - startTime) / 1000;
        log.info("本次ETH区块同步任务结束，耗时={}秒 height={}", spent, height);
    }

    public static BigDecimal toEth(String value) {
        BigDecimal bigDecimal = new BigDecimal(value);
        return bigDecimal.divide(BlockConstant.WEI, 8, RoundingMode.HALF_UP);
    }

    public static BigDecimal usdtToEth(String value) {
        BigDecimal bigDecimal = new BigDecimal(value);
        return bigDecimal.divide(BlockConstant.USDT_ERC20_RATIO, 8, RoundingMode.HALF_UP);
    }

    private CoinInfoDto containsErc20(List<CoinInfoDto> contractAddresses, String toAddress) {
        for (CoinInfoDto coinConfig : contractAddresses) {
            if (coinConfig.getCoinContractAddress().equals(toAddress)) {
                return coinConfig;
            }
        }
        return null;
    }

}
